package org.example.searcher;

import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;

// 通过这个类，来完成整个的搜索过程
public class DocSearcher {

    // 停用词表路径
    private static String STOP_PATH = null;

    static {
        if (Config.isOnline) {
            STOP_PATH = "/www/wwwroot/doc_searcher_index/stop_word.txt";
        }
        else {
            STOP_PATH = "D:\\java_develop_doucement\\stop_word.txt";
        }
    }

    // 使用 HashSet 存储停用词
    private HashSet<String> stopWords = new HashSet<>();


    // 此处要加上索引对象的实例，同时完成索引加载的工作
    private Index index = new Index();
    public DocSearcher() {
        index.load();
    }

    // 完成整个搜索过程的方法
    // 参数(输入部分) 就是用户给的查询词
    // 返回值(输出部分) 就是搜索结果的集合
    public List<Result> search(String query) {
        // 1. [分词] 针对 query 这个查询词进行分词
        List<Term> oldTerms = ToAnalysis.parse(query).getTerms();
        List<Term> terms = new ArrayList<>();
        // 针对分词结果，使用暂停表进行过滤
        for (Term term : oldTerms) {
            if (stopWords.contains(term.getName())) {
                continue;
            }
            terms.add(term);
        }

        // 2. [触发] 针对分词结果来查倒排
        List<List<Weight>> termResult = new ArrayList<>();
        for (Term term : terms) {
            String word = term.getName();
            List<Weight> invertedList = index.getInverted(word);
            if (invertedList == null) {
                continue;
            }
            termResult.add(invertedList); // 这里将该单词解析出来的所有Weight对象加入到总列表中
        }
        // 3. [合并] 针对多个分词结果触发出来的相同文档，进行权重合并
        List<Weight> allTermResults = mergeResult(termResult);

        // 4. [排序] 针对触发的结果按照权重降序排序;
        allTermResults.sort(new Comparator<Weight>() {
            @Override
            public int compare(Weight o1, Weight o2) {
                return o2.getWeight() - o1.getWeight(); // 降序
            }
        });
        // 5. [包装结果] 针对排序的结果，去查正排，构造出要返回的数据
        List<Result> results = new ArrayList<>();
        for (Weight weight : allTermResults) {
            DocInfo docInfo = index.getDocInfo(weight.getDocId());
            Result result = new Result();
            result.setTitle(docInfo.getTitle());
            result.setUrl(docInfo.getUrl());
            result.setDesc(genDesc(docInfo.getContent(), terms));
            results.add(result);
        }
        return results;
    }

    // 通过这个内部类，来描述一个元素在二维数组中的位置
    static class Pos {
        public int row;
        public int col;

        public Pos(int row, int col) {
            this.row = row;
            this.col = col;
        }
    }

    private List<Weight> mergeResult(List<List<Weight>> source) {
        // 在进行合并时，是把多个行合并成一行
        // 合并过程中涉及到二维数组
        // 操作元素就涉及到了行和列的概念

        // 1. 先针对每一行进行排序(按照id进行升序排序)
        for (List<Weight> curRow : source) {
            curRow.sort(new Comparator<Weight>() {
                @Override
                public int compare(Weight o1, Weight o2) {
                    return o1.getDocId() - o2.getDocId();
                }
            });
        }
        // 2. 借助一个优先队列，针对这些进行合并
        // target 表示合并的结果
        List<Weight> target = new ArrayList<>();
        // 创建一个优先级队列，并指定比较规则(按照 Weight 的 docId，取小的优先)
        PriorityQueue<Pos> queue = new PriorityQueue<>(new Comparator<Pos>() {
            @Override
            public int compare(Pos o1, Pos o2) {
                // 先根据 pos 值找到对应的 Weight 对象，再根据 Weight 的 docId 来排序
                Weight w1 = source.get(o1.row).get(o1.col);
                Weight w2 = source.get(o2.row).get(o2.col);
                return w1.getDocId() - w2.getDocId();
            }
        });
        // 2.2 初始化对列，把每一行的第一个元素放到队列中
        for (int row = 0;row < source.size();row++) {
            // 初始插入的元素的 col 就是 0
            queue.offer(new Pos(row, 0));
        }
        // 2.3 循环的取队首元素(也就是当前这若干行中最小的元素)
        while (!queue.isEmpty()) {
            Pos minPos = queue.poll();
            Weight curWeight = source.get(minPos.row).get(minPos.col);
            // 2.4 看看这个取到的 Weight 是否和前一个插入到 target 中的结果是相同的 docId，如果是，就合并
            if (target.size() > 0) {
                // 取出上次插入的元素
                Weight lastWeight = target.get(target.size() - 1);
                if (lastWeight.getDocId() == curWeight.getDocId()) {
                    // 进入这里说明遇到了相同的文档，合并权重
                    lastWeight.setWeight(lastWeight.getWeight() + curWeight.getWeight());
                }
                else {
                    // 如果文档 id 不相同，就直接把 curWeight 给插入到 target 的末尾
                    target.add(curWeight);
                }
            }else {
                // 如果当前 target 是空着的，就直接插入即可
                target.add(curWeight);
            }
            // 2.5 把当前元素处理完了之后，要把对应这个元素的光标往后移动，去取这一行的下一个元素
            Pos newPos = new Pos(minPos.row, minPos.col + 1);
            if (newPos.col >= source.get(newPos.row).size()) {
                // 如果移动光标之后，超出了这一行的列数，就说明到达末尾了
                // 到达末尾之后说明这一行就处理完毕了
                continue;
            }
            queue.offer(newPos);
        }
        return target;
    }


    private String genDesc(String content, List<Term> terms) {
        // 先遍历分词结果，看看哪个结果在content中存在
        int firstPos = -1;
        for (Term term : terms) {
            String word = term.getName();
            // 注，这里的term已经被分词库全部处理成了小写，在查询时需要把正文也转成小写再进行查询
            // 同时这里需要实现 “全字匹配”，让word能够独立成词，而不是作为词的一部分，可以使用正则表达式解决
            content = content.toLowerCase().replaceAll("\\b" + word + "\\b", " " + word + " ");
            firstPos = content.indexOf(" " + word + " ");
            if (firstPos >= 0) {
                // 这里仅需要查询出一个词在正文中出现即可
                break;
            }
        }
        if (firstPos == -1) {
            // 表示所有的分词结果在正文中不存在
            if (content.length() > 160) {
                return content.substring(0, 160) + "...";
            }
            return content;
        }
        // 以firstPos作为基准位置，往前找任意字符，作为描述的起始位置
        String desc = "";
        int descBeg = firstPos < 60 ? 0 : firstPos - 60;
        if (descBeg + 160 > content.length()) {
            desc = content.substring(descBeg) + "...";
        }
        else {
            desc = content.substring(descBeg, descBeg + 160) + "...";
        }

        // 在此处加上一个替换成操作，把描述词中和分词结果相同的部分，加上一层<i>标签，就可以通过replace的方式来实现
        for (Term term : terms) {
            String word = term.getName();
            // 此处要进行全字匹配
            desc = desc.replaceAll("(?i)" + word + " ", "<i> " + word + " </i>");
        }

        return desc;
    }

    public void loadStopWords() {
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(STOP_PATH))) {
            while (true) {
                String line = bufferedReader.readLine();
                if (line == null) {
                    // 读取文件完毕
                    break;
                }
                stopWords.add(line);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        DocSearcher docSearcher = new DocSearcher();
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("-> ");
            String query = scanner.next();
            List<Result> results = docSearcher.search(query);
            for (Result result : results) {
                System.out.println("======================");
                System.out.println(result);
            }
        }
    }

}
